Persisting the other Bouned Contexts
Implement a DDD domain model using Spring Data JPA, Hibernate, H2, and Lombok, including inheritance, entity relationships, and collections of Value Objects.
Overview
In Activity 03, you learned how a DDD domain model can be mapped to a relational database using JPA and Hibernate. You learned how to persist entities, Value Objects, enumerations, and repositories.
In this activity, you will apply these concepts to another bounded context of the Library Management System: the Collection Management Context.
This time, you will do more of the implementation yourself. You will receive a DDD class diagram in PlantUML format and will translate the model into Java and JPA code.
In particular, this activity introduces three important persistence concepts:
- Entity inheritance.
- One-to-many relationships between entities.
- One-to-many relationships between an entity and a Value Object.
Learning Goals
By the end of this activity, you should be able to:
- Translate a DDD class diagram into Java classes.
- Map an abstract domain class to a JPA entity.
- Map entity inheritance using JPA.
- Use a discriminator column to distinguish subclasses.
- Map Value Objects using
@Embeddableand@Embedded. - Implement a one-to-many relationship between entities using
@OneToMany. - Understand the difference between an Entity-to-Entity relationship and an Entity-to-Value Object relationship.
- Implement a collection of Value Objects using
@ElementCollection. - Explain why a collection of Value Objects cannot simply be mapped using
@Embedded. - Use Lombok to reduce boilerplate code while preserving domain behavior.
- Verify the resulting database schema using the H2 Console.
Before You Begin
You should already have:
- Completed Activity 03.
- The Library Management System Spring Boot project.
- The required Spring Data JPA dependencies.
- Lombok configured correctly.
- H2 configured correctly.
- A working understanding of
@Entity,@Id,@Embeddable, and@Embedded.
Deliverables
At the end of the activity, your project should contain a working implementation of the Collection Management Context.
- The
LibraryItemabstract entity. - The
Bookentity. - The
AudioMaterialentity. - The
VideoMaterialentity. - The
Authorentity. - The required Value Objects.
- The required enumerations.
- The JPA inheritance mapping.
- The Book-to-Author one-to-many relationship.
- The Author-to-PhoneNumber collection of Value Objects.
- The required Spring Data JPA repository interfaces.
- A repository for the LibraryItem inheritance hierarchy.
- A repository for Book.
- A repository for Author.
- A successfully running Spring Boot application.
- A database schema visible in the H2 Console.
Part 1 : Study the Collection Management Domain Model
Before writing any code, study the PlantUML diagram provided by your instructor. The diagram represents the Collection Management bounded context of the Library Management System.
Before continuing, identify the following elements in the diagram:
| Question | Your Answer |
|---|---|
| Which class is abstract? | ____________________________ |
| Which three classes inherit from it? | ____________________________ |
| Which classes are Entities? | ____________________________ |
| Which classes are Value Objects? | ____________________________ |
| Which entity has multiple Authors? | ____________________________ |
| Which entity has multiple PhoneNumbers? | ____________________________ |
Create the multi-layer architecture
As, we have done before, Create the multi-layer architecture in the collection managenment context as follows:
Part 2 : Mapping an Abstract Entity and Its Subclasses
The first important feature of the Collection Management
context is inheritance.
The domain model contains an abstract class:
LibraryItem.
It contains information common to all library materials.
Three concrete classes inherit from it:
BookAudioMaterialVideoMaterial
Create the class hierarchy using ordinary class inheritance as follows:
public abstract class LibraryItem {
...
}
public class Book extends LibraryItem {
...
}
public class AudioMaterial extends LibraryItem {
...
}
public class VideoMaterial extends LibraryItem {
...
}
However, Java inheritance is not enough when the objects must be persisted. Hibernate also needs to know how the inheritance hierarchy should be represented in the database.
JPA Inheritance
JPA provides the @Inheritance annotation for
this purpose.
In this activity, we will use the
SINGLE_TABLE inheritance strategy.
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "item_type")
public abstract class LibraryItem {
...
}
The subclasses are also JPA entities:
@Entity
@DiscriminatorValue("BOOK")
public class Book extends LibraryItem {
...
}
@Entity
@DiscriminatorValue("AUDIO")
public class AudioMaterial extends LibraryItem {
...
}
@Entity
@DiscriminatorValue("VIDEO")
public class VideoMaterial extends LibraryItem {
...
}
What Does SINGLE_TABLE Mean?
With the SINGLE_TABLE strategy, Hibernate stores
all objects in the inheritance hierarchy in one database table.
library_items
--------------------------------
item_id
title
status
publication_year
item_type
isbn
duration
narrator
format
age_rating
--------------------------------
BOOK → Book-specific columns
AUDIO → Audio-specific columns
VIDEO → Video-specific columns
item_type column is the discriminator.
It tells Hibernate which Java subclass should be created
when a row is retrieved.
Your Task
Implement the inheritance hierarchy from the supplied PlantUML diagram.
You must:
- Make
LibraryIteman abstract JPA entity. - Configure JPA inheritance.
- Add a discriminator column.
- Make
Booka JPA entity. - Make
AudioMateriala JPA entity. - Make
VideoMateriala JPA entity. - Assign discriminator values to the three subclasses.
Part 3 : Mapping Value Objects
The Collection Management context contains several Value Objects. Examples include:
ItemIdISBNDurationPublicationYearAddressPhoneNumberAuthorId
As you learned in Activity 03, a Value Object normally does not
have its own identity and therefore does not need to be mapped
as an @Entity.
A Value Object that is stored as part of one entity can be mapped
using @Embeddable and @Embedded.
Example: Address
The Author contains one Address.
Therefore, the Address can be embedded directly into the
Author table.
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class Address {
private String street;
private String city;
private String zipCode;
}
The Author can then contain:
@Embedded
private Address address;
Conceptually:
Author object │ ├── authorId ├── name ├── address │ ├── street │ ├── city │ └── zipCode │ └── phoneNumbers
The Address fields can be stored as columns in the Author table.
@Embedded works well when an entity contains
one Value Object of a particular type.
Part 4 : Entity-to-Entity One-to-Many Relationship
The Collection Management domain model contains the following relationship:
Book "1" o-- "0..*" Author
This means that one Book can have zero or more
Author objects.
Unlike a Value Object, an Author is an Entity.
It has its own identity:
Author
|
+-- AuthorId
+-- name
+-- address
+-- phoneNumbers
There is a special annotation to speciy this entity relationship.
The JPA Annotation
JPA provides the @OneToMany annotation for
this relationship.
The Book will contain a collection:
private List<Author> authors;
The relationship can be mapped as follows:
@OneToMany
private List authors;
In our domain model, Authors belong to the Book aggregate. Therefore, the lifecycle of the Authors is controlled by the Book. We can express this using cascading and orphan removal.
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
@JoinColumn(name = "book_id")
private List authors = new ArrayList<>();
What Does Cascade Mean?
CascadeType.ALL tells JPA that persistence
operations performed on the Book should also be propagated
to its Authors.
For example, when a new Book containing new Authors is persisted,
the Authors can also be persisted.
What Does orphanRemoval Mean?
Consider:
book.removeAuthor(author);
With orphanRemoval = true, an Author removed
from the Book's collection can be removed from the database
because it is no longer part of the aggregate.
cascade = CascadeType.ALL simply because
it is convenient. Cascade behavior should reflect the ownership
and lifecycle defined by the domain model.
What Does @JoinColumn(name = "book_id") Mean?
The @JoinColumn annotation tells JPA which database column
should be used to represent the relationship between a Book
and its Author objects.
In our example:
@OneToMany(
cascade = CascadeType.ALL,
orphanRemoval = true
)
@JoinColumn(name = "book_id")
private List<Author> authors = new ArrayList<>();
The name = "book_id" part specifies that Hibernate should
create a book_id column in the authors table.
This column stores the database identifier of the Book
to which each author belongs.
Conceptually, the resulting tables look like this:
library_items
-------------------------
id
item_uuid
title
item_status
publication_year
item_type
...
authors
-------------------------
id
author_uuid
name
...
book_id
For example, suppose the database contains:
library_items
id item_type title
1 BOOK Clean Code
authors
id name book_id
10 Robert C. Martin 1
11 Another Author 1
The value book_id = 1 means that both authors are associated
with the Book whose database identifier is 1.
Notice that @JoinColumn does not create a separate
book_authors join table. Instead, the foreign-key column
book_id is stored directly in the authors table.
Your Task
Implement the Book-to-Author relationship represented in the PlantUML diagram.
You must:
- Declare the collection of Authors in
Book. - Use
@OneToMany. - Initialize the collection.
- Use appropriate cascade behavior.
- Use
orphanRemovalif the Author is owned by the Book aggregate. - Implement
addAuthor(). - Implement
removeAuthor(). - Implement
getAuthors().
Author an Entity rather than a Value Object?
What makes an Author different from an ISBN or PhoneNumber?
Part 5 : Entity-to-Value Object One-to-Many Relationship
Now we encounter a different kind of relationship. An Author can have multiple phone numbers:
Author "1" o-- "0..*" PhoneNumber
At first glance, this may look similar to the Book-to-Author relationship. However, there is an important difference.
Author is an Entity.
PhoneNumber is a Value Object.
Therefore, a PhoneNumber does not need its own identity
and should not be mapped as an @Entity.
Why Can't We Use @Embedded?
Suppose we tried to write:
@Embedded
private PhoneNumber phoneNumber;
This would allow an Author to have only one PhoneNumber. But our domain model says:
private List<PhoneNumber> phoneNumbers;
An Author can have:
PhoneNumber 1
PhoneNumber 2
PhoneNumber 3
...
A relational table cannot place an arbitrary number of PhoneNumber objects into one column.
@Embedded is designed for embedding the fields
of one Value Object into the owning entity.
It is not the correct annotation for a collection of
Value Objects.
The Correct JPA Mapping
JPA provides @ElementCollection specifically
for collections of basic values or Value Objects.
First, define PhoneNumber as an embeddable class:
@Embeddable
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PhoneNumber {
private String number;
public boolean isValid() {
...
}
}
Then map the collection in Author:
@ElementCollection
private List phoneNumbers = new ArrayList<>();
Hibernate will create a separate table for the collection.
## authors
author_id
name
street
city
zip_code
--------
## author_phone_numbers
author_id
number
------
The author_phone_numbers table does not represent
an Entity called PhoneNumber.
It represents the collection of PhoneNumber Value Objects
owned by Authors.
Understanding the Difference
| Domain Relationship | JPA Mapping | Separate Entity Identity? |
|---|---|---|
| Author → Address | @Embedded |
No |
| Book → Author | @OneToMany |
Yes |
| Author → PhoneNumber | @ElementCollection |
No |
Your Task
Implement the Author-to-PhoneNumber relationship.
You must:
- Make
PhoneNumberan@Embeddableclass. - Do not make
PhoneNumberan@Entity. - Declare a collection of PhoneNumbers in
Author. - Use
@ElementCollection. - Initialize the collection.
- Implement
addPhoneNumber(). - Implement
removePhoneNumber(). - Implement
getPhoneNumbers().
Part 6 : Implement the Collection Management Context
You are now ready to implement the complete domain model. We provided you with the complete PlantUML diagram. Use it as the specification for your implementation.
Step 1 : Create the Package Structure
Create the Collection Management bounded context using the same package organization used in Activity 03.
collectionmanagement
│
├── domainlayer
│ ├── entity
│ └── valueobject
│
├── infrastructurelayer
│ └── repository
│ ├── LibraryItemRepository
│ ├── BookRepository
│ └── AuthorRepository
│
├── businesslogiclayer
│ └── services
│
└── presentationlayer
└── controllers
Step 2 : Implement the Value Objects
Implement the Value Objects shown in the diagram.
ItemIdISBNDurationPublicationYearMaterialFormatAddressPhoneNumberAuthorId
Decide which classes require @Embeddable.
@Embeddable when JPA needs to persist its fields.
Special Problem with value and year Attributes
When we use @Embeddable Value Objects, Hibernate maps their
attributes to columns in the database. However, some of the attribute
names we use in our Java classes can cause problems because they are
reserved words or special keywords in SQL/H2.
For example, our AuthorId Value Object may contain an
attribute called value:
@Embeddable
public class AuthorId {
private UUID value;
}
Similarly, our PublicationYear Value Object may contain an
attribute called year:
@Embeddable
public class PublicationYear {
private int year;
}
When Hibernate creates the database schema, these Java attribute names become database column names. It could therefore generate SQL such as:
value uuid
year integer
The problem is that value and year can have
special meaning in H2/SQL. As a result, H2 may reject the generated
CREATE TABLE statement.
We can solve this problem by using the @Column annotation
to explicitly specify safe database column names.
For AuthorId, we can write:
@Embeddable
public class AuthorId {
@Column(name = "author_uuid")
private UUID value;
}
The Java attribute is still called value, but Hibernate will
use author_uuid as the database column name.
Similarly, for PublicationYear:
@Embeddable
public class PublicationYear {
@Column(name = "publication_year")
private int year;
}
Hibernate will therefore create a column called
publication_year instead of year.
This illustrates an important distinction: the Java attribute name and
the database column name do not have to be the same. The
@Column(name = "...") annotation allows us to explicitly
control the database representation of an entity or Value Object
attribute.
Step 3 : Implement LibraryItem
Implement the abstract LibraryItem class.
It should contain the fields specified by the diagram:
id // primary key
itemId
title
status
publicationYear
Configure it as the root of the JPA inheritance hierarchy. It should also contain the abstract method:
public abstract String getDescription();
Step 4 : Implement the Three Subclasses
Implement:
BookAudioMaterialVideoMaterial
Each class must extend LibraryItem and implement
getDescription().
Step 5 : Implement Author
Implement the Author entity according to the
supplied diagram.
Pay particular attention to:
- The Author identity.
- The embedded Address.
- The collection of PhoneNumbers.
Step 6 : Implement the Book-to-Author Relationship
Add the collection of Authors to Book.
Use:
@OneToMany
and configure the relationship according to the aggregate ownership represented by the domain model.
Step 7 : Implement the Author-to-PhoneNumber Collection
Add the collection of PhoneNumbers to Author.
Use:
@ElementCollection
Do not use:
@Embedded
for the collection itself.
Part 7 : Use Lombok Carefully
You may use Lombok to reduce boilerplate code.
For example:
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
can be used on JPA entities.
You should not automatically use @Data on every
entity.
toString() methods can
create problems with domain behavior and JPA relationships.
In particular, be careful with entities containing collections such as:
private List<Author> authors;
private List phoneNumbers;
Your domain methods should control important state changes.
public void addAuthor(Author author) {
authors.add(author);
}
public void removeAuthor(Author author) {
authors.remove(author);
}
and:
public void addPhoneNumber(PhoneNumber phone) {
phoneNumbers.add(phone);
}
public void removePhoneNumber(PhoneNumber phone) {
phoneNumbers.remove(phone);
}
Part 8 : Create the Repository Interfaces
The domain model is now mapped to JPA entities and Value Objects. However, the application also needs a way to communicate with the database.
In Spring Data JPA, repositories provide the persistence operations required to save, retrieve, update, and delete entities without having to write the SQL queries ourselves.
LibraryItem,
Book, and Author, but not for
ISBN, Address, PhoneNumber,
or the other Value Objects.
Repository Package
In the infrastructurelayer package, create the
repository package:
collectionmanagement
│
├── domainlayer
│ ├── entity
│ └── valueobject
│
├── infrastructurelayer
│ └── repository
│ ├── LibraryItemRepository.java
│ ├── BookRepository.java
│ └── AuthorRepository.java
│
├── businesslogiclayer
│ └── services
│
└── presentationlayer
└── controllers
LibraryItemRepository
Because LibraryItem is the root of the inheritance
hierarchy, we can define a repository for the abstract entity.
public interface LibraryItemRepository
extends JpaRepository<LibraryItem, Long> {
}
Notice that the repository uses LibraryItem as its
entity type and Long as its primary-key type.
The primary-key type must correspond to the type of the
@Id field in LibraryItem.
LibraryItem primary key uses a different Java
type, such as UUID, replace Long with
that type.
BookRepository
We can also define a repository specifically for Book.
This is useful when the application needs to perform operations
specifically on Books.
public interface BookRepository
extends JpaRepository<Book, Long> {
}
Because Book is part of the JPA inheritance hierarchy,
this repository still uses the same primary-key type as
LibraryItem.
AuthorRepository
The Author class is also an Entity and therefore
requires a repository if the application needs to access Authors
independently.
public interface AuthorRepository
extends JpaRepository<Author, Long> {
}
Here, Long represents the type of the
@Id field of the Author entity.
Use the actual primary-key type defined in your implementation.
Long or AuthorId
into the repository declarations. The second generic parameter
of JpaRepository<T, ID> must exactly match the
Java type of the field annotated with @Id.
Required Imports
Each repository needs the Spring Data JPA repository interface and the corresponding entity:
import org.springframework.data.jpa.repository.JpaRepository;
For example, LibraryItemRepository will also import
LibraryItem from the domain entity package.
Why Are the Repository Interfaces Empty?
At first, the repository interfaces may appear to contain no useful code:
public interface BookRepository
extends JpaRepository<Book, Long> {
}
However, by extending JpaRepository, the interface
automatically inherits common persistence operations such as:
save()findById()findAll()existsById()deleteById()count()
Spring Data JPA automatically creates an implementation of the repository interface when the application starts.
BookRepository
|
v
JpaRepository<Book, Long>
|
+-- save()
+-- findById()
+-- findAll()
+-- deleteById()
+-- count()
|
v
Spring Data JPA
|
v
Hibernate
|
v
H2 Database
Repository and Value Objects
Notice that we did not create repositories for Value Objects.
ISBNRepository // Do not create
AddressRepository // Do not create
PhoneNumberRepository // Do not create
DurationRepository // Do not create
These objects do not have independent identity in the domain. They are persisted as part of the Entity that owns them.
For example, PhoneNumbers are persisted through the
Author entity using @ElementCollection.
They do not require a separate repository.
Your Task
Create the repository interfaces required by the Collection Management Context.
- Create the
repositorypackage. - Create
LibraryItemRepository. - Create
BookRepository. - Create
AuthorRepository. - Make each repository extend
JpaRepository. - Use the correct Entity type as the first generic parameter.
- Use the correct primary-key type as the second generic parameter.
- Do not create repositories for Value Objects.
Checkpoint
Before continuing, verify that your project contains:
infrastructurelayer
└── repository
├── LibraryItemRepository.java
├── BookRepository.java
└── AuthorRepository.java
Part 9 : Predict the Database Schema
Before running the application, predict what tables Hibernate should create. Based on your mappings, complete the following table.
| Domain Concept | Expected Database Representation |
|---|---|
| LibraryItem hierarchy | ____________________________ |
| Book | ____________________________ |
| Author | ____________________________ |
| Address | ____________________________ |
| PhoneNumber collection | ____________________________ |
Important Question
Why does the collection of PhoneNumbers require a separate database table even though PhoneNumber is a Value Object?
A relational table row has a fixed number of columns. An Author can have zero, one, two, or many PhoneNumbers. Therefore, the collection must be represented separately.
Part 10 : Run the Application
Run your Spring Boot application using IntelliJ or:
./gradlew bootRun
If the application fails during startup, carefully examine the error message.
Common causes include:
- Missing
@Entity. - Missing no-argument constructor.
- Incorrect inheritance configuration.
- Incorrect relationship annotation.
- A Value Object incorrectly declared as an Entity.
- A collection of Value Objects incorrectly mapped using
@Embedded. - Incorrect package placement.
Part 11 : Verify the Database Using H2 Console
Open:
http://localhost:8080/h2-console
Use the JDBC URL configured in your project.
Inspect the Inheritance Table
Find the table corresponding to the
LibraryItem hierarchy.
Verify that it contains a discriminator column.
For example:
ITEM_TYPE
This column allows Hibernate to distinguish between:
BOOK
AUDIO
VIDEO
Inspect the Author Table
Verify that the Author table contains the Author fields and the fields belonging to the embedded Address.
Inspect the Phone Number Table
Find the table created for the PhoneNumber collection. It should contain a way to associate each PhoneNumber with its owning Author.
PhoneNumber Entity.
Nevertheless, Hibernate creates a table for the collection
of PhoneNumber Value Objects.
Part 12: finish the Loan Management Context
Now, you have all the knowledge and tools to build another bounded context. Your final task is to complete the Loan Management Context at home, following the UML diagram below.
The goal is not simply to reproduce the classes shown in the diagram. You must also make the Loan Management Context persistent using JPA/Hibernate, just as you did for the Collection Management Context.
Check Your Understanding
Answer the following questions before completing the activity.
-
Why is
LibraryIteman abstract class? -
Why must
LibraryItemstill be mapped as a JPA entity even though it is abstract? -
What is the purpose of
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)? - What is the purpose of the discriminator column?
- What is the difference between an Entity and a Value Object?
-
Why is the Book-to-Author relationship mapped using
@OneToMany? -
Why is the Author-to-PhoneNumber relationship not mapped
using
@OneToMany? -
Why can't we use
@Embeddedfor aList<PhoneNumber>? -
What does
@ElementCollectiontell Hibernate? - Why does Hibernate create a separate table for a collection of Value Objects?
-
What is the purpose of
cascade = CascadeType.ALLin the Book-to-Author relationship? -
What is the purpose of
orphanRemoval = true? -
Why should we avoid automatically using
@Dataon JPA entities?
Troubleshooting
Problem: Hibernate does not recognize a subclass
Check that the subclass has @Entity and that it
extends the abstract JPA entity.
Problem: The inheritance hierarchy is not mapped correctly
Check that the root entity contains:
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
Also check the discriminator configuration.
Problem: PhoneNumber causes a JPA mapping error
Make sure that PhoneNumber is annotated with
@Embeddable and that the collection in
Author uses @ElementCollection.
Problem: I used @OneToMany for PhoneNumber
Remember that PhoneNumber is a Value Object. It does not have its own identity and therefore should not be mapped as a JPA Entity.
Problem: Hibernate cannot create the Author relationship
Check the @OneToMany configuration and make sure
that the relationship corresponds to the domain model.
Problem: Hibernate complains about a missing constructor
JPA entities and embeddable classes need an accessible no-argument constructor.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Reflection
In this activity, you encountered three different persistence situations:
Entity inheritance
|
v
@OneToMany
|
v
@ElementCollection
These three situations may look similar because they all involve relationships between Java objects, but they represent different domain concepts.
Consider the following question:
Why is it important to understand the difference between an Entity and a Value Object before choosing a JPA annotation?
Consider another question:
An Author can have many PhoneNumbers. Why does this not automatically mean that PhoneNumber should be an Entity?
Finally:
What would happen if you changed PhoneNumber from a Value Object into an Entity? How would that change the database design and the domain model?
Up Next
In this activity, you translated a DDD model into a persistent object model and learned how different domain relationships require different JPA mappings.
In the next activity, we will use repositories and application services to work with actual persistent objects.
DDD Model
|
v
Java Classes
|
v
JPA Mapping
|
+----------------------+
| | |
v v v
Inheritance @OneToMany @ElementCollection
| | |
+----------+-----------+
|
v
Hibernate
|
v
H2 Database
Appendix A : Collection Management JPA Mapping
| DDD Concept | JPA Representation |
|---|---|
| Entity | @Entity |
| Abstract Entity | @Entity + abstract |
| Entity inheritance | @Inheritance |
| Single-table inheritance | @Inheritance(strategy = InheritanceType.SINGLE_TABLE) |
| Inheritance discriminator | @DiscriminatorColumn |
| Subclass discriminator value | @DiscriminatorValue |
| Value Object | @Embeddable |
| One Value Object | @Embedded |
| Entity-to-Entity one-to-many | @OneToMany |
| Collection of Value Objects | @ElementCollection |
| Aggregate ownership | cascade / orphanRemoval |
Do not choose a JPA annotation simply because two Java classes are related.
First identify the DDD relationship:
Entity + Entity
→ @OneToMany
Entity + one Value Object
→ @Embedded
Entity + collection of Value Objects
→ @ElementCollection